Learning Domain Driven Design - 6. Tackling Complex Business Logic
Learning Domain Driven Design
前回はシンプルなビジネスロジックに対するパターン
Transaction Script
ActiveRecord
今回は複雑なビジネスロジックに対するDDDパターンについて
まとめ
ドメインモデルパターンの紹介
より複雑なビジネスロジックに適している
Value Object, Aggregates, Domain Service から構成される
歴史を簡単に紹介
Patterns of Enterprise Application Architecture という本で Martin Fowler はドメインモデルパターンを紹介した
Domain-Driven Design で Eric Evans がそれを発展させ、Aggregation や Value Object をを導入した
Domain Model
複雑なビジネスロジックを管理するために利用する設計パターン
振る舞いとデータをカプセル化するオブジェクトモデル
ValueObject, Aggregation, Domain Service を利用する
複雑なビジネスロジック (= ActiveRecord を使用して実装するのが難しい) の例
e.g. ヘルプデスクシステムの設計
顧客がチケットを作成
顧客とエージェントがメッセージを追加できる
チケットには優先度に基づいた時間制限がある
制限内に応答がない場合、エスカレートされ、再度超えると別のエージェントに割り当てられる
顧客が 7日以内に反応しない場合自動で閉じる
マネージャー、顧客のみチケットを閉じられる
顧客は7日以内に閉じられたものなら再度開ける
Domain Model は、データベース呼び出しやシステムの外部コンポーネントなど技術的な問題は取り扱わない
モデルのオブジェクトはプレーンなオブジェクトであり、インフラのコンポーネントやフレームワークに直接依存はしない
技術的な問題よりもビジネスロジックを優先することで、ユビキタス言語との整合性を保ちやすくなる
つまり、コードがユビキタス言語を話すことが可能になる
構成要素 Value Object, Aggregates, Domain Service それぞれについて説明していく
Value Object
その値の組み合わせで識別されるオブジェクト。明示的なIDを持つ必要がない
e.g. Color Object
code:java
class Color
{
int _red;
int _green;
int _blue;
}
Primitive Obsession: プリミティブな値のみのフィールドでドメインを表現するコードスメル
正確な値が入ることを保証するため、全ての入力を検証する必要があるが、検証ロジックが色々な場所に重複したり、値を利用する前に検証ロジックの呼び出しを強制するのが難しい
code:java
class Person
{
private int _id;
private string _firstName;
private string _lastName;
private string _countryCode;
public Person(...) {...}
}
ebiken.icon mesh だと validator で済ませている
https://github.com/go-playground/validator
code:go
type Sample struct {
A string validate:"required"
}
func New(a string) (*Sample, error) {
s := &Sample{
A: a,
}
if err := validate.Struct(s); err != nil {
return nil, err
}
return s, nil
}
// ng
s1 := Sample{A: "hoge"}
var s2 Sample
s3 := new(Sample)
// ok
s4, _ := New("fuga")
カスタムのバリデーション pkg/validate みたいなパッケージを切って、カスタムで tag を定義している。couponcode みたいな
Value Object はそれに対する解決策として有用
明確さが向上し、Value Object が検証ロジックをカプセル化しているため代入前に検証する必要がなくなる
値の操作に関するビジネスロジックも検証し、テストも容易になる
また、この Value Object が表現する概念はユビキタス言語と一致する
code:java
class Person {
private PersonId _id;
private Name _name;
private CountryCode _country;
public Person(...) { ... }
}
immutable である必要がある
意図しない副作用を防ぐ、スレッドセーフにするため
また、等価性 equality も正しく実装する必要がある
.NET や Java など多くの言語では、文字列型が Value Object として実装されている
Entities
Value Object と異なり、一意に識別できる ID をもつオブジェクト
mutable
プロパティに Value Object を利用できる
Aggregates
Entity 一つ以上からなるもので、整合性を保つもの
整合性を保つため、他の Aggregate と明確な境界を引く
Aggregate の外部からは
状態を読むことのみが許される
変更は公開 i/f の実行 (Command とも呼ばれる) でのみ可能
code:java
public class Ticket
{
...
public void Execute(AddMessage cmd)
{
var message = new Message(cmd.from, cmd.body);
_messages.Append(message);
}
...
}
Aggregate は並行に実行されても整合性を担保する必要がある
トランザクション境界としても機能する
-> 一つ一つの Aggregate を個々のトランザクションとしてコミットできる
トランザクション境界でまとめた例
https://gyazo.com/c6375e5426536b3090829987e821cfe6
https://gyazo.com/bf84eb0b119e3437445ab8fcbc453d1c
この図における Ticket が Aggregate Root と呼ばれるものになる
Aggregate の中で一つの Entity が i/f を公開する形になる
code:java
public class Ticket
{
...
List<Message> _messages;
...
public void Execute(EvaluateAutomaticActions cmd)
{
if (this.IsEscalated && this.RemainingTimePercentage < 0.5 &&
GetUnreadMessagesCount(for: AssignedAgent) > 0)
{
_agent = AssignNewAgent();
}
}
public int GetUnreadMessagesCount(UserId id)
{
return _messages.Where(x => x.To == id && !x.WasRead).Count();
}
...
}
Value Object と同様、Aggregate の名前 (public i/f, field) はユビキタス言語と一致しているべき
Domain Expert とコミュニケーションしやすくなる
Domain Events
Aggregate Root の public i/f 以外に、Aggregate とコミュニケーションする方法の一つ
https://gyazo.com/fa7199466392ea9b469b82f4a4ce91d2
e.g.
Ticket assigned
Ticket escalated
Message received
ebiken.icon実際は application/usecase 層を設けてそこに transaction 境界を持ってくるケースが多そう
transaction 境界を分けたときに整合性を保つのが大変。domain event を使うとインフラ周りの複雑度が増して運用が大変になってくる
Domain Services
システムの拡大に伴い、ビジネスロジックが特定の Aggregate や Value Object に明確に分類されず、複数の Aggregate に関連しているように見えるケースが出てくるはず。この場合にそのロジックを Domain service として実装するのを提案する
Stateless
複数のシステムコンポーネントへの呼び出しを調整する
これは microservice, service oriented architecture, その他のサービスとは無関係で、単なるビジネスロジックをホストする stateless なオブジェクトにすぎないので注意
code:java
public class ResponseTimeFrameCalculationService
{
...
public ResponseTimeframe CalculateAgentResponseDeadline(UserId agentId,
Priority priority, bool escalated, DateTime startTime)
{
var policy = _departmentRepository.GetDepartmentPolicy(agentId);
var maxProcTime = policy.GetMaxResponseTimeFor(priority);
if (escalated) {
maxProcTime = maxProcTime * policy.EscalationFactor;
}
var shifts = _departmentRepository.GetUpcomingShifts(agentId,
startTime, startTime.Add(policy.MaxAgentResponseTime));
return CalculateTargetTime(maxProcTime, shifts);
}
}
複雑さを管理する
ここまで紹介してきた Aggregate, Value Object, Domain Service はドメインの複雑さに対処するための手段
オブジェクトの自由度を下げ、immutable にすることで複雑さを減らすことができる
まとめ
ビジネスロジックの複雑さを、境界を区切ってカプセル化することで対処する
Value Objects
その値でのみ一意に識別できる(IDをもたない)ビジネスドメイン
Aggregates
トランザクション境界で区切られ、強力な整合性を持つ
public i/f でのみ更新できる (= command)
domain event を使うことでも他の境界ともやりとりできる
Domain Services
stateless で複数の value object, aggregate を管理するもの
次の章では、"時間" という次元を取り入れたより高度な方法を学ぶ
event sourcing